import fs from 'fs-extra'
import path from 'path'
import util from 'util'
import { ReplaceRuleCfg, RuleParser } from './RuleParser.js'

interface TypeInfo {
    type: string
    fullType?: string
    namespace: string
}

export class Convertor {
    private readonly typeMapper: { [type: string]: string } = {
        'UnityEngine.Type': 'System.Type',
        'UnityEngine.UI.UIText': 'UIText',
        'UnityEngine.UI.UITextUrl': 'UITextUrl',
        'UnityEngine.ShadowCastingMode': 'UnityEngine.Rendering.ShadowCastingMode',
        'Game.UIPolygon': 'Game.UIPolygon',
        'Game.KeyCode': 'UnityEngine.KeyCode',
        'Game.AssetPriority': 'AssetPriority',
        'Game.UrlAssetType': 'UrlAssetType',
        'UnityEngine.UI.TextAnchor': 'UnityEngine.TextAnchor'
    }

    private ruleCfgs: ReplaceRuleCfg[] = []
    private readonly csTypes: TypeInfo[] = []

    public async start (projectRoot: string, ruleFile?: string) : Promise<void> {
        if (ruleFile != null) {
            const parser = new RuleParser()
            this.ruleCfgs = await parser.read(ruleFile)
            // console.log(util.inspect(this.ruleCfgs, false, 4))
        }
        await this.collectUts(projectRoot)
        const tsRoot = path.join(projectRoot, 'TsScripts')
        await this.processTs(tsRoot)
    }

    private async collectUts (projectRoot: string): Promise<void> {
        const wf = path.join(projectRoot, 'Assets/Scripts/uts/StaticWrap/WrapFiles.cs')
        const content = await fs.readFile(wf, 'utf-8')
        const r1 = content.matchAll(/_GT\("([^"]+)",typeof\([^"]+\),"([^"]+)"\),?/mg)
        for (const r of r1) {
            let fullType: string | undefined = undefined
            if (r[2].includes('.')) fullType = r[2]
            this.csTypes.push({ type: `${r[1]}.${r[2]}`, namespace: r[1], fullType })
        }
        const r2 = content.matchAll(/_GT\("([^"]+)",typeof\(([^"]+)\)\),?/mg)
        for (const r of r2) {
            let fullType: string | undefined = undefined
            const names = r[2].split('.')
            if (names.length > 1) fullType = r[2]
            this.csTypes.push({ type: `${r[1]}.${names[names.length - 1]}`, namespace: r[1], fullType })
        }
    }

    private async processTs (tsRoot: string): Promise<void> {
        const files = await fs.readdir(tsRoot)
        for (const f of files) {
            const file = path.join(tsRoot, f)
            const fstat = await fs.stat(file)
            if (fstat.isDirectory()) {
                await this.processTs(file)
            } else if (path.extname(f) === '.ts') {
                const oldContent = await fs.readFile(file, 'utf-8')
                let newContent = oldContent
                for (const type of this.csTypes) {
                    let newType = this.typeMapper[type.type] ?? type.fullType
                    if (newType == null) {
                        if (type.namespace === 'FFTween' || type.namespace === 'Game') {
                            const narr = type.type.split('.')
                            newType = narr[narr.length - 1]
                        } else {
                            newType = type.type
                        }
                    }
                    newType = 'CS.' + newType
                    const re = new RegExp('(?<!CS\\.)\\b' + type.type.split('.').join('\\.') + '\\b', 'g')
                    newContent = newContent.replaceAll(re, newType)
                    newContent = newContent.replaceAll(newType + '.GetType()', (substring, ...args) => {
                        return `puerts.$typeof(${substring.substring(0, substring.length - 10)})`
                    })
                    // 数组
                    newContent = newContent.replaceAll(` as ${newType}[]`, '')
                    newContent = newContent.replaceAll(new RegExp(':\\s\\b' + newType.split('.').join('\\.') + '\\b\\[\\](?=\\s*=\\s*\\w+\\.GetComponents)', 'g'), '')
                }
                // 部分衍生类处理
                newContent = newContent.replaceAll(/\bFFTween\b\./g, 'CS.')
                newContent = newContent.replaceAll(/(?<!CS\.)\bUnityEngine\b\./g, 'CS.UnityEngine.')
                newContent = newContent.replaceAll(/(?<!CS\.)\bGame\b\.(?!Vector(2|3))/g, 'CS.Game.')
                for (const key in this.typeMapper) {
                    newContent = newContent.replaceAll('CS.' + key, 'CS.' + this.typeMapper[key])
                }

                // newContent = newContent.replaceAll('uts.logError', 'console.error')
                // newContent = newContent.replaceAll('uts.logFailure', 'console.error')
                // newContent = newContent.replaceAll('uts.logWarning', 'console.warn')
                // newContent = newContent.replaceAll('uts.logs', 'console.log')
                // newContent = newContent.replaceAll('uts.log', 'console.log')
                // newContent = newContent.replaceAll('uts.assert', 'console.assert')
                // $ref
                newContent = newContent.replaceAll(/(?<=Tools\.(?:GetPosition|GetGameObjectPosition|GetGameObjectLocalPosition|GetAnchoredPosition|GetGameObjectAnchoredPosition|GetRectSize|GetGameObjectRectSize)\([^,]+,\s+?).+(?=\))/mg, (substring, ...args) => {
                    if (substring.startsWith('puerts.$ref')) return substring
                    return `puerts.$ref(${substring})`
                })
                newContent = newContent.replaceAll(/(?<=\.ScreenPointToLocalPointInRectangle\([^,]+,\s+?[^,]+,\s+?[^,]+,\s+?).+(?=\))/mg, (substring, ...args) => {
                    if (substring.startsWith('puerts.$ref')) return substring
                    return `puerts.$ref(${substring})`
                })
                // Equals
                newContent = newContent.replaceAll(/([\w.]+)\.Equals\(null\)/g, (substring, ...args) => {
                    return `CS.UnityEngine.Object.op_Equality(${args[0]}, null)`
                })
                // scrollPanel
                newContent = newContent.replaceAll(' as CS.UnityEngine.UI.FyScrollRect', ' as unknown as CS.UnityEngine.UI.FyScrollRect')
                newContent = newContent.replaceAll(/([\w.]*[Ss]croll\w*)\.onValueChanged = delegate\(this, this.(\w+)\)/g, (substring, ...args) => {
                    const call = args[1]
                    if (newContent.search(new RegExp('\\b' + call + '\\b\\s*\\(\\)')) >= 0) {
                        return `(${args[0]}.onValueChanged as unknown as CS.UnityEngine.Events.UnityEvent).AddListener(()=>this.${call}());`
                    }
                    return `(${args[0]}.onValueChanged as unknown as CS.UnityEngine.Events.UnityEvent).AddListener((change?: CS.UnityEngine.Vector2)=>this.${call}(change));`
                })
                newContent = newContent.replaceAll(/([\w.]*[Ss]croll\w*)\.onValueChanged = (\w+)/g, (substring, ...args) => {
                    const call = args[1]
                    return `(${args[0]}.onValueChanged as unknown as CS.UnityEngine.Events.UnityEvent).AddListener(${call});`
                    // if (newContent.search(new RegExp('\\b' + call + '\\b\\s*\\(\\)')) >= 0) {
                    //     return `(${args[0]}.onValueChanged as unknown as CS.UnityEngine.Events.UnityEvent).AddListener(()=>this.${call}());`
                    // }
                    // return `(${args[0]}.onValueChanged as unknown as CS.UnityEngine.Events.UnityEvent).AddListener((change?: CS.UnityEngine.Vector2)=>this.${call}(change));`
                })
                // slider
                newContent = newContent.replaceAll(/([\w.]*[Ss]lider\w*)\.onValueChanged = delegate\(this, this.(\w+)\)/g, (substring, ...args) => {
                    const call = args[1]
                    if (newContent.search(new RegExp('\\b' + call + '\\b\\s*\\(\\)')) >= 0) {
                        return `(${args[0]}.onValueChanged as unknown as CS.UnityEngine.Events.UnityEvent).AddListener(()=>this.${call}());`
                    }
                    return `(${args[0]}.onValueChanged as unknown as CS.UnityEngine.Events.UnityEvent).AddListener((num?: number)=>this.${call}(num));`
                })
                // toggle
                newContent = newContent.replaceAll(/([\w.]*[Tt]oggle\w*)\.onValueChanged = delegate\(this, this.(\w+)\)/g, (substring, ...args) => {
                    const call = args[1]
                    if (newContent.search(new RegExp('\\b' + call + '\\b\\s*\\(\\)')) >= 0) {
                        return `(${args[0]}.onValueChanged as unknown as CS.UnityEngine.Events.UnityEvent).AddListener(()=>this.${call}());`
                    }
                    return `(${args[0]}.onValueChanged as unknown as CS.UnityEngine.Events.UnityEvent).AddListener((val?: boolean)=>this.${call}(val));`
                })
                // array helper
                newContent = newContent.replaceAll(/CS\.ArrayHelper\.GetArrayLength\((\w+)\)/g, `$1.Length`)
                newContent = newContent.replaceAll(/CS\.ArrayHelper\.GetArrayValue\((\w+),\s*(\w+)\)/g, `$1.get_Item($2)`)
                const imports: string[] = []
                for (const cfg of this.ruleCfgs) {
                    if (file.includes(cfg.file)) {
                        for (const rule of cfg.rules) {
                            newContent = newContent.replaceAll(rule.searchValue, rule.replaceValue)
                        }
                        if (cfg.imports != null) {
                            for (const ipt of cfg.imports) {
                                if (!newContent.includes(ipt)) imports.push(ipt)
                            }
                        }
                    }
                }
                if (imports.length > 0) {
                    if (newContent[0] === '\ufeff') {
                        newContent = newContent[0] + imports.join('\n') + '\n' + newContent.substring(1);
                    } else {
                        newContent = imports.join('\n') + '\n' + newContent;
                    }
                }
                if (newContent !== oldContent) {
                    await fs.writeFile(file, newContent, 'utf-8')
                }
            }
        }
    }
}
